package com.example.spider.testing;

import com.example.spider.util.HtmlUtil;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

/**
 * （基于文本行块分布的）通用文本内容抽取算法
 * 比较适合于新闻和Blog的正文抽取
 * 算法：《基于行块分布函数的通用网页正文抽取》复杂度: O(n)、不建立dom树、不依赖html标签
 * 算法步骤：
 * 1. 预处理（编码、去脚本、特殊字符抓换）
 * 2. 去除格式标签，得到粗糙正文
 * 3. 求行块字数分布函数
 * 4. 从函数中找到骤升、骤降点，得到目标正文
 * 5. 最终格式修正
 * 相关链接：谷歌论文：https://code.google.com/p/cx-extractor/  PDF: http://cx-extractor.googlecode.com/files/%E5%9F%BA%E4%BA%8E%E8%A1%8C%E5%9D%97%E5%88%86%E5%B8%83%E5%87%BD%E6%95%B0%E7%9A%84%E9%80%9A%E7%94%A8%E7%BD%91%E9%A1%B5%E6%AD%A3%E6%96%87%E6%8A%BD%E5%8F%96.pdf
 * 其它转载网站链接：https://max.book118.com/html/2014/0318/6687226.shtm
 */
public class TextDistributionBaseGeneralContextExtractor {

    /**
     * 行块大小，取3。
     * 文章段落间可能有空行，取3可以避免【段落标题 + 空行 + 段落】布局时，段落标题字太少导致匹配丢失问题
     */
    private static final int STANDARD_TEXT_BLOCK_LINE_NUM = 3;

    /**
     * 文章最小内容文本长度。如果正文字数太少，非常容易被网页其他段落替代，如更多推荐、评论，因此该算法适合正文内容文字不太少的文章。
     * 取值时保证该值目标网页正文最小长度、大于目标网页非正文内容最大长度，100-200之间一般
     */
    private static final int MIN_CONTENT_LENGTH = 100;

    /**
     * 大文本块字符数阈值
     */
    private static final int BIG_TEXT_BLOCK_WORD_NUM = 40;

    /**
     * 算法无法给出高置信度结果时，返回默认内容
     */
    private static final String DEFAULT_CONTENT = "网页可能为非主题型网页，目前暂不处理。";

    /**
     * 提取正文带标签（如图片、格式等）
     */
    public static String extractContextStr(String sourceText) {
        TextBlock textBlock = extractContextTextBlock(sourceText);
        return textBlock == null ? DEFAULT_CONTENT : textBlock.text;
    }

    /**
     * 提取正文带标签（如图片、格式等）
     */
    public static String extractContextStrWithTag(String sourceText) {
        TextBlock textBlock = extractContextTextBlock(sourceText);
        return textBlock == null ? DEFAULT_CONTENT : textBlock.sourceText;
    }

    /**
     * 提取正文带标签（如图片、格式等）
     */
    public static TextBlock extractContextTextBlock(String sourceText) {
        // 预处理
        String sourceTextWithoutTag = HtmlUtil.removeAllHtmlTag(sourceText);
        // 与处理后的保持对其
        String sourceTextAfterProcessNewLine = HtmlUtil.replaceHumanNewLine(sourceText);
        // 中文
        if (!isContentPage(sourceTextWithoutTag)) {
            return null;
        }

        // 将文本转为 textBlockList
        List<TextBlock> textBlocks = toTextBlock(sourceTextAfterProcessNewLine, sourceTextWithoutTag);

        // 找到目标文本块
        return findContentInTextBlocks(textBlocks);
    }

    /**
     * 将文本转为文本块
     *
     * @return List<TextBlock>
     */
    public static List<TextBlock> toTextBlock(String sourceText, String sourceTextWithoutTag) {
        // 这里不能使用 \n+ 因为这样会破坏文本分布信息
        String[] sourceLines = sourceText.split("\n");
        String[] linesWithoutTag = sourceTextWithoutTag.split("\n");
        List<TextLine> textLines = new ArrayList<>(linesWithoutTag.length);
        for (int i = 0; i < linesWithoutTag.length; i++) {
            String line = linesWithoutTag[i];
            String sourceLine = sourceLines[i];
            TextLine textLine = new TextLine();
            textLine.sourceLine = sourceLine;
            textLine.text = line;
            textLine.wordNum = line.replaceAll("\\s+", "").length();
            textLines.add(textLine);
        }

        // 将字少的、孤伶伶的单行文本改为空字符串（上下两行都没有字）
        cleanSingleTextLine(textLines);
        // 计算所在文本块字符长度
        calculateBlockWordNum(textLines);

        // 将所有行转为文本块（保留有字的行，合并连续的行）
        List<TextBlock> textBlockList = mergeContinuousTextToTextBlock(textLines);
        // 合并连续的大块文本
        mergeClosedBigTextBlock(textBlockList);

        return textBlockList;
    }

    /**
     * 找到目标文本块
     * @param textBlockList
     * @return
     */
    private static TextBlock findContentInTextBlocks(List<TextBlock> textBlockList) {
        return textBlockList.stream()
                .filter(textBlock -> {
                    textBlock.text = textBlock.text.trim();
                    textBlock.blockWordNum = textBlock.text.replaceAll("\\s+", "").length();
                    return textBlock.blockWordNum > MIN_CONTENT_LENGTH;
                })
                .max(Comparator.comparingInt(line -> line.blockWordNum))
                .orElse(null);
    }

    /**
     * 合并连续的大块文本（两块只差两个空行，两块都比较多），以弥补单纯抽取最大块的缺点
     *
     * @param textBlocks 初步划分的文本块
     */
    private static void mergeClosedBigTextBlock(List<TextBlock> textBlocks) {
        for (int i = 1; i < textBlocks.size(); i++) {
            TextBlock currentTextBlock = textBlocks.get(i);
            TextBlock lastTextBlock = textBlocks.get(i - 1);

            // 和上一块文本块连续
            boolean continuousWithLastTextBlock = currentTextBlock.beginIndex == lastTextBlock.endIndex + 1;
            boolean continuousBigMultiLineTextBlock = continuousWithLastTextBlock
                    // 本块内容行数较多
                    && currentTextBlock.biggerThanStandardBlockLineNum()
                    // 本块字数较多
                    && currentTextBlock.blockWordNum > BIG_TEXT_BLOCK_WORD_NUM;

            // 上一个块是小块,且字少
            boolean lastBlockIsSmallTextBlock = lastTextBlock.endIndex == lastTextBlock.beginIndex + STANDARD_TEXT_BLOCK_LINE_NUM
                    // 上一块字很少
                    && lastTextBlock.blockWordNum < BIG_TEXT_BLOCK_WORD_NUM;
            if (!continuousBigMultiLineTextBlock || lastBlockIsSmallTextBlock) {
                // 否则仍然把上一块看作单独的文本块（未来舍弃）
                continue;
            }
            // 既然这俩是连续的大块文本，就和上一块文本合并
            lastTextBlock.sourceText += currentTextBlock.sourceText;
            lastTextBlock.text += currentTextBlock.text;
            lastTextBlock.endIndex = currentTextBlock.endIndex;
            textBlocks.remove(i);
            --i;
        }
    }

    private static List<TextBlock> mergeContinuousTextToTextBlock(List<TextLine> textLines) {
        List<TextBlock> textBlockList = new ArrayList<>();
        for (int i = 0; i < textLines.size(); i++) {
            if (textLines.get(i).wordNum == 0) {
                // 这行没字，不算
                continue;
            }
            StringBuilder sourceTextBuilder = new StringBuilder();
            StringBuilder textBuilder = new StringBuilder();
            TextBlock textBlock = new TextBlock();
            textBlock.beginIndex = i;
            // 合并连续有字的行
            while (i < textLines.size() && textLines.get(i).wordNum > 0) {
                sourceTextBuilder.append(textLines.get(i).sourceLine).append("\n");
                textBuilder.append(textLines.get(i).text).append("\n");
                textBlock.blockWordNum += textLines.get(i).wordNum;
                // 不弥补 i-- 也可以，因为这行是空行，下次 for 循环也要跳过
                i++;
            }
            textBlock.endIndex = i;
            textBlock.sourceText = sourceTextBuilder.toString();
            textBlock.text = textBuilder.toString();
            textBlockList.add(textBlock);

        }
        return textBlockList;
    }

    private static void cleanSingleTextLine(List<TextLine> textLines) {
        for (int i = 0; i + 4 < textLines.size(); i++) {
            if (textLines.get(i).wordNum == 0
                    && textLines.get(i + 1).wordNum == 0
                    && textLines.get(i + 2).wordNum > 0 && textLines.get(i + 2).wordNum < 40
                    && textLines.get(i + 3).wordNum == 0
                    && textLines.get(i + 4).wordNum == 0) {

                textLines.get(i + 2).text = "";
                textLines.get(i + 2).wordNum = 0;
                i += 3;
            }
        }
    }

    private static void calculateBlockWordNum(List<TextLine> textLines) {
        for (int i = 0; i < textLines.size() - STANDARD_TEXT_BLOCK_LINE_NUM; i++) {
            // 本行字数
            textLines.get(i).blockWordNum += textLines.get(i).wordNum;
            for (int j = i + 1; j < i + STANDARD_TEXT_BLOCK_LINE_NUM && j < textLines.size(); j++) {
                // 后两行字数
                textLines.get(i).blockWordNum += textLines.get(j).wordNum;
            }
        }
    }

    /**
     * Checks if is content page.
     *
     * @param htmlText the html text
     * @return true, if is content page
     */
    private static boolean isContentPage(String htmlText) {
        int count = 0;
        for (int i = 0; i < htmlText.length() && count < 5; i++) {
            if (htmlText.charAt(i) == '，' || htmlText.charAt(i) == '。')
                count++;
        }
        return count >= 5;
    }

    public static class TextLine {
        // 本行原始文本
        public String sourceLine;
        // 本行文本
        public String text;
        // 本行包含字数
        public int wordNum;
        // 本块包含字数
        public int blockWordNum;
    }

    public static class TextBlock {

        public String sourceText;

        public String text;

        public int beginIndex;

        public int endIndex;
        // 本块包含字数
        public int blockWordNum;

        public boolean biggerThanStandardBlockLineNum() {
            return endIndex - beginIndex > STANDARD_TEXT_BLOCK_LINE_NUM;
        }
    }

}
